﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace BorgExt
{
    public class AssemblyAnalyzer
    {
        private static readonly IDictionary<Type, IDictionary<Type, IList<Attribute>>> AllAttributes =
            new Dictionary<Type, IDictionary<Type, IList<Attribute>>>();

        static AssemblyAnalyzer()
        {
            AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += OnReflectionOnlyAssemblyResolve;
        }

        public static bool Analyze(string path, TypeBuilder typeBuilder,
            out Contract contract, out Plugin plugin)
        {
            var assembly = GetAssembly(path);

            if (SkipAssembly(assembly))
            {
                contract = null;
                plugin = null;
                return false;
            }
            
            contract = GetContract(assembly, typeBuilder);
            plugin = GetPlugin(assembly, typeBuilder);

            return contract != null || plugin != null;
        }

        public static bool Analyze(Assembly assembly, TypeBuilder typeBuilder,
            out Contract contract, out Plugin plugin)
        {
            if (SkipAssembly(assembly))
            {
                contract = null;
                plugin = null;
                return false;
            }
            
            contract = GetContract(assembly, typeBuilder);
            plugin = GetPlugin(assembly, typeBuilder);

            return contract != null || plugin != null;
        }

        public static Assembly GetAssembly(string path)
        {
            return Assembly.ReflectionOnlyLoadFrom(path);
        }

        public static bool IsAssemblyContract(Assembly assembly)
        {
            if (assembly == null)
                throw new ArgumentNullException("assembly");
            
            return InternalIsAssemblyContract(assembly);
        }

        public static bool IsAssemblyPlugin(Assembly assembly)
        {
            if (assembly == null)
                throw new ArgumentNullException("assembly");
            
            return InternalIsAssemblyPlugin(assembly);
        }

        private static Contract GetContract(Assembly assembly, TypeBuilder typeBuilder)
        {
            var slotDefs = new List<SlotDefinition>();

            foreach (var type in assembly.GetTypes())
            {
                var customAttributes = GetCustomAttributes<SlotDefinitionAttribute>(type);

                if (customAttributes.Count != 0)
                    slotDefs.AddRange(GetSlotDefinitions(customAttributes, type, typeBuilder));
            }

            return slotDefs.Count > 0
                ? typeBuilder.CreateContract(assembly.GetName().Name, assembly, slotDefs)
                : null;
        }

        private static ICollection<SlotDefinition> GetSlotDefinitions(
            ICollection<SlotDefinitionAttribute> slotDefAttrs, Type type, TypeBuilder typeBuilder)
        {
            var customAttributes = GetCustomAttributes<ParamDefinitionAttribute>(type);
            var slotDefs = new List<SlotDefinition>(slotDefAttrs.Count);
            
            slotDefs.AddRange(
                from attribute in slotDefAttrs
                let paramDefinitions = GetParamDefinitions(customAttributes, typeBuilder)
                select typeBuilder.CreateSlotDefinition(attribute, type, paramDefinitions));
            
            return slotDefs;
        }

        private static ICollection<ParamDefinition> GetParamDefinitions(
            ICollection<ParamDefinitionAttribute> paramDefAttrs, TypeBuilder typeBuilder)
        {
            var paramDefs = new List<ParamDefinition>(paramDefAttrs.Count);
            
            paramDefs.AddRange(paramDefAttrs.Select(typeBuilder.CreateParamDefinition));

            return paramDefs;
        }

        private static Plugin GetPlugin(Assembly assembly, TypeBuilder typeBuilder)
        {
            var extTypes = new List<ExtensionType>();

            foreach (var type in assembly.GetTypes())
            {
                var customAttributes = GetCustomAttributes<ExtensionAttribute>(type);
                
                if (customAttributes.Count != 0)
                    extTypes.AddRange(GetExtensionTypes(customAttributes, type, typeBuilder));
            }

            return extTypes.Count > 0
                ? typeBuilder.CreatePlugin(assembly.GetName().Name, assembly, extTypes)
                : null;
        }

        private static ICollection<ExtensionType> GetExtensionTypes(
            ICollection<ExtensionAttribute> extensionAttrs, Type type, TypeBuilder typeBuilder)
        {
            var plugAttrs = GetCustomAttributes<PlugAttribute>(type);
            var slotAttrs = GetCustomAttributes<SlotAttribute>(type);
            var paramAttrs = GetCustomAttributes<ParamAttribute>(type);
            var extTypes = new List<ExtensionType>(extensionAttrs.Count);
            
            extTypes.AddRange(
                from extAttr in extensionAttrs
                let plugTypes = GetPlugTypes(plugAttrs, paramAttrs, typeBuilder)
                let slotTypes = GetSlotTypes(slotAttrs, typeBuilder)
                select typeBuilder.CreateExtensionType(extAttr, type, plugTypes, slotTypes));

            return extTypes;
        }

        private static ICollection<PlugType> GetPlugTypes(ICollection<PlugAttribute> plugAttrs,
            ICollection<ParamAttribute> paramAttrs, TypeBuilder typeBuilder)
        {
            var plugTypes = new List<PlugType>(plugAttrs.Count);
            
            plugTypes.AddRange(
                from plugAttr in plugAttrs
                let parameters = GetParams(paramAttrs, plugAttr.Name, typeBuilder)
                select typeBuilder.CreatePlugType(plugAttr, parameters));

            return plugTypes;
        }

        private static ICollection<Param> GetParams(ICollection<ParamAttribute> paramAttrs,
            string plugName, TypeBuilder typeBuilder)
        {
            return
                (from paramAttr in paramAttrs
                 where string.IsNullOrEmpty(paramAttr.Plug) || paramAttr.Plug == plugName
                 select typeBuilder.CreateParam(paramAttr)).ToList();
        }

        private static ICollection<SlotType> GetSlotTypes(ICollection<SlotAttribute> slotAttrs, TypeBuilder typeBuilder)
        {
            var slotTypes = new List<SlotType>(slotAttrs.Count);
            
            slotTypes.AddRange(slotAttrs.Select(typeBuilder.CreateSlotType));
            
            return slotTypes;
        }

        private static bool InternalIsAssemblyContract(Assembly assembly)
        {
            return assembly.GetTypes().Any(HasCustomAttribute<SlotDefinitionAttribute>);
        }

        private static bool InternalIsAssemblyPlugin(Assembly assembly)
        {
            return assembly.GetTypes().Any(HasCustomAttribute<ExtensionAttribute>);
        }

        internal static Assembly OnReflectionOnlyAssemblyResolve(object sender, ResolveEventArgs args)
        {
            return Assembly.ReflectionOnlyLoad(args.Name);
        }

        internal static string GetAssemblyTitle(Assembly a)
        {
            return ((AssemblyTitleAttribute)a.GetCustomAttributes(
                typeof(AssemblyTitleAttribute), false)[0]).Title;
        }

        internal static string GetAssemblyProduct(Assembly a)
        {
            return ((AssemblyProductAttribute)a.GetCustomAttributes(
                typeof(AssemblyProductAttribute), false)[0]).Product;
        }

        internal static string GetAssemblyInformationalVersion(Assembly a)
        {
            return ((AssemblyInformationalVersionAttribute)a.GetCustomAttributes(
                typeof(AssemblyInformationalVersionAttribute), false)[0]).InformationalVersion;
        }

        internal static string GetAssemblyFileVersion(Assembly a)
        {
            return ((AssemblyFileVersionAttribute)a.GetCustomAttributes(
                typeof(AssemblyFileVersionAttribute), false)[0]).Version;
        }

        internal static string GetAssemblyCopyright(Assembly a)
        {
            return ((AssemblyCopyrightAttribute)a.GetCustomAttributes(
                typeof(AssemblyCopyrightAttribute), false)[0]).Copyright;
        }

        private static bool HasCustomAttribute<T>(MemberInfo type)
        {
            return CustomAttributeData.GetCustomAttributes(type).Any(
                customAttributeData => customAttributeData.Constructor.DeclaringType.
                    AssemblyQualifiedName == typeof (T).AssemblyQualifiedName);
        }

        public static IDictionary<Type, IList<Attribute>> GetAllCustomAttributes(Type type)
        {
            var attributes = new Dictionary<Type, IList<Attribute>>();

            if (type.Assembly.ReflectionOnly)
                foreach (var cad in CustomAttributeData.GetCustomAttributes(type))
                    AddAttribute(attributes, DeserializeAttribute<Attribute>(cad));
            else
                foreach (var attr in Attribute.GetCustomAttributes(type))
                    AddAttribute(attributes, attr);

            return attributes;
        }

        private static void AddAttribute(Dictionary<Type, IList<Attribute>> attributes, Attribute attr)
        {
            for (var type = attr.GetType(); typeof(Attribute).IsAssignableFrom(type); type = type.BaseType)
            {
                IList<Attribute> attrs;

                if (!attributes.TryGetValue(type, out attrs))
                {
                    attrs = new List<Attribute>();
                    attributes.Add(type, attrs);
                }

                attrs.Add(attr);
            }
        }

        public static List<T> GetCustomAttributes<T>(Type type)
            where T : Attribute
        {
            IDictionary<Type, IList<Attribute>> customAttributes;

            if (!AllAttributes.TryGetValue(type, out customAttributes))
            {
                customAttributes = GetAllCustomAttributes(type);
                AllAttributes.Add(type, customAttributes);
            }

            var list = new List<T>();
            IList<Attribute> attrs;

            if (customAttributes.TryGetValue(typeof (T), out attrs))
                list.AddRange(customAttributes[typeof (T)].Cast<T>());

            return list;
        }

        private static T DeserializeAttribute<T>(CustomAttributeData cad)
        {
            var types = new Type[cad.ConstructorArguments.Count];
            var parameters = new object[cad.ConstructorArguments.Count];
            
            for (var i = 0; i < parameters.Length; ++i)
            {
                var attributeTypedArgument = cad.ConstructorArguments[i];
                types[i] = Type.GetType(attributeTypedArgument.ArgumentType.AssemblyQualifiedName);
                parameters[i] = GetValue(attributeTypedArgument);
            }

            var type = Type.GetType(cad.Constructor.DeclaringType.AssemblyQualifiedName);
            var o = (T)type.GetConstructor(types).Invoke(parameters);
            
            foreach (var attributeNamedArgument in cad.NamedArguments)
            {
                switch (attributeNamedArgument.MemberInfo.MemberType)
                {
                    case MemberTypes.Field:
                        type.GetField(attributeNamedArgument.MemberInfo.Name).
                            SetValue(o, GetValue(attributeNamedArgument.TypedValue));
                        break;
                    
                    case MemberTypes.Property:
                        type.GetProperty(attributeNamedArgument.MemberInfo.Name).
                            SetValue(o, GetValue(attributeNamedArgument.TypedValue), null);
                        break;
                }
            }

            return o;
        }

        private static object GetValue(CustomAttributeTypedArgument arg)
        {
            var type = arg.Value.GetType();
            var argumentType = arg.ArgumentType;
            
            if (argumentType.IsAssignableFrom(type))
                return arg.Value;
            
            var qType = Type.GetType(argumentType.AssemblyQualifiedName);
            
            if (argumentType.IsArray)
            {
                if (argumentType.GetArrayRank() != 1)
                    throw new ArrayTypeMismatchException("Only one-dimensional arrays are supported.");

                var list = arg.Value as IList;

                if (list != null)
                {
                    var count = list.Count;
                    var obj = qType.GetConstructor(new[] { typeof (int) }).Invoke(new object[] { count });
                    var oList = obj as IList;

                    for (var i = 0; i < count; ++i)
                        if (oList != null)
                            oList[i] = GetValue((CustomAttributeTypedArgument) list[i]);

                    return obj;
                }
            }

            if (argumentType.IsEnum)
                return Enum.Parse(qType, ((int)arg.Value).ToString());
            
            throw new Exception("Unable to obtain value of " + arg);
        }

        private static bool SkipAssembly(Assembly assembly)
        {
            return HasAssemblyAttribute(assembly, typeof(NoAssemblyAnalyzerAttribute));
        }

        public static bool HasAssemblyAttribute(Assembly assembly, Type attributeType)
        {
            return CustomAttributeData.GetCustomAttributes(assembly).Any(
                a => attributeType.AssemblyQualifiedName ==
                    a.Constructor.DeclaringType.AssemblyQualifiedName);
        }
    }
}

